﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.IO;

namespace Cjc.Data.Mapping
{
	public static class ExtensionMethods
	{
		#region Mapping context helpers

		public static IMappingContextInfo GetMappingContextInfo( this Type contextType )
		{
			var mappingContextAttrib = contextType.GetAttribute<MappingContextAttribute>();

			if ( mappingContextAttrib != null && mappingContextAttrib.MappingContextInfoType != null )
			{
				return (IMappingContextInfo)Activator.CreateInstance( mappingContextAttrib.MappingContextInfoType );
			}

			return null;
		}

		#endregion

		#region Property mapping helpers

		public static MemberInfo GetKnownProperty( this Type modelType, KnownProperty knownProperty )
		{
			var groups = from p in modelType.GetProperties( BindingFlags.Instance | BindingFlags.Public )
						 let g = p.GetCustomAttributes( typeof( PropertyGroupAttribute ), true ).Cast<PropertyGroupAttribute>().FirstOrDefault()
						 where g != null
						 select new { Property = p, Group = g };

			foreach ( var g in groups )
			{
				var found = g.Property.PropertyType.GetKnownProperty( knownProperty );

				if ( found != null ) return found;
			}

			return modelType.GetProperties().FirstOrDefault( p =>
				{
					var attrib = p.GetAttribute<PropertyMappingAttribute>();

					return ( attrib != null ) ? attrib.KnownProperty == knownProperty : false;
				} );
		}

		public static MemberInfo GetMappedProperty( this MemberInfo member, Type entityType )
		{
			var propertyMapping = member.GetAttribute<PropertyMappingAttribute>();

			return ( propertyMapping != null )
				? entityType.GetMember( propertyMapping.Storage ).FirstOrDefault()
				: null;
		}

		#endregion

		#region Type mapping helpers

		public static TypeMappingAttribute GetTypeMapping( this Expression value )
		{
			return ( value != null ) ? value.Type.NullableType().GetTypeMapping() : null;
		}

		public static TypeMappingAttribute GetTypeMapping( this Type type )
		{
			return type.GetAttribute<TypeMappingAttribute>();
		}

		public static Expression ConvertFromData( this Expression value, TypeMappingAttribute convert )
		{
			return value.ConvertFromData( convert, null );
		}

		public static Expression ConvertFromData( this Expression value, TypeMappingAttribute convert, Type preferredType )
		{
			var method = convert.GetConvertFromDataMethod( value.Type );

			return ( method != null && ( preferredType == null || preferredType.IsAssignableFrom( method.ReturnType ) ) )
				? method.CoerceCall( value )
				: value;
		}

		public static Expression ConvertToData( this Expression value, TypeMappingAttribute convert )
		{
			var method = convert.GetConvertToDataMethod( value.Type );

			return ( method != null )
				? method.CoerceCall( value )
				: value;
		}

		public static MethodInfo GetConvertFromDataMethod( this TypeMappingAttribute convert, Type type )
		{
			return convert.Type.GetMethod( convert.FromDataMethod, new[] { type } )
				?? convert.Type.GetMethod( convert.FromDataMethod, new[] { type.NullableType() } )
				?? convert.Type.GetMethod( convert.FromDataMethod, Type.EmptyTypes );
		}

		public static MethodInfo GetConvertToDataMethod( this TypeMappingAttribute convert, Type type )
		{
			return convert.Type.GetMethod( convert.ToDataMethod, new[] { type } )
				?? convert.Type.GetMethod( convert.ToDataMethod, new[] { type.NullableType() } )
				?? convert.Type.GetMethod( convert.ToDataMethod, Type.EmptyTypes );
		}

		public static Expression CoerceCall( this MethodInfo method, params Expression[] parameters )
		{
			var methodParams = method.GetParameters();
			var index = 0;
			var coerced = parameters.Select( p =>
			{
				var type = methodParams[ index++ ].ParameterType;

				return ( p.Type != type )
					? Expression.Convert( p, type )
					: p;
			} ).ToArray();

			return Expression.Call( method, coerced );
		}

		#endregion

		#region Xml mapping helpers

		public static XmlStorageMappingAttribute GetXmlStorageMapping( this Type modelType )
		{
			return modelType.GetAttribute<XmlStorageMappingAttribute>();
		}

		#endregion

		#region Miscellaneous

		public static T GetAttribute<T>( this MemberInfo member )
			where T : Attribute
		{
			return ( member != null )
				? member.GetCustomAttributes( typeof( T ), true ).Cast<T>().FirstOrDefault()
				: null;
		}

		public static T GetAttribute<T>( this Type type )
			where T : Attribute
		{
			return ( type != null )
				? type.GetCustomAttributes( typeof( T ), true ).Cast<T>().FirstOrDefault()
				: null;
		}

		public static Expression InlineInvokes( this Expression expression )
		{
			return ExpressionVisitor<InvocationExpression>.Visit( expression,
				i => ( i.Expression is LambdaExpression )
					? MakeInline( (LambdaExpression)i.Expression, i.Arguments.ToArray() )
					: i );
		}

		public static Expression MakeInline( this LambdaExpression lambda, params Expression[] parameters )
		{
			var index = 0;
			var replacements = lambda.Parameters.Select( p => new { Param = p, Value = parameters[ index++ ] } )
				.ToDictionary( p => p.Param, p => p.Value );

			return ExpressionVisitor<ParameterExpression>.Visit( lambda.Body,
				p => replacements.ContainsKey( p ) ? replacements[ p ] : p );
		}

		#endregion

		#region ReadOnlyCollection extensions

		// Methods
		internal static ReadOnlyCollection<T> ToReadOnlyCollection<T>( this IEnumerable<T> sequence )
		{
			return new ReadOnlyCollection<T>( ( sequence != null ) ? sequence.ToList() : (IList<T>)new T[ 0 ] );
		}

		#endregion

		#region Nullable type helpers

		/// <summary>
		/// Determines whether the specified type is nullable.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns>
		/// 	<c>true</c> if the specified type is nullable; otherwise, <c>false</c>.
		/// </returns>
		internal static bool IsNullable( this Type type )
		{
			return ( type != null && type.IsGenericType && type.GetGenericTypeDefinition().Equals( typeof( Nullable<> ) ) );
		}

		/// <summary>
		/// Get the underlying type.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns></returns>
		internal static Type NullableType( this Type type )
		{
			return IsNullable( type ) ? new NullableConverter( type ).UnderlyingType : type;
		}

		#endregion
	}
}